/**
 * User: alesj
 * Date: 2005.1.20
 * Time: 14:46:57
 * 
 * (C) Genera Lynx d.o.o.
 */

package com.generalynx.ecos.beans;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
import org.springframework.beans.propertyeditors.ClassEditor;
import org.springframework.core.Constants;

import java.util.*;

public abstract class DependencyInjectionAspectSupport implements InitializingBean, BeanFactoryAware {

    protected final Log logger = LogFactory.getLog(getClass());

    private BeanFactory beanFactory;

    private AutowireCapableBeanFactory aabf;

    /**
     * Map of Class to prototype name
     */
    private Map managedClassToPrototypeMap = new HashMap();

    private int defaultAutowireMode = 0;

    private Constants constants = new Constants(AutowireCapableBeanFactory.class);

    /**
     * List of Class
     */
    protected List autowireByTypeClasses = new LinkedList();

    /**
     * List of Class
     */
    private List autowireByNameClasses = new LinkedList();

    /**
     * @return Returns the autowireAll.
     */
    public int getDefaultAutowireMode() {
        return defaultAutowireMode;
    }

    /**
     * @param constantName name of the constant
     * @see #setDefaultAutowireMode
     */
    public void setDefaultAutowireModeName(String constantName) {
        setDefaultAutowireMode(constants.asNumber(constantName).intValue());
    }

    /**
     * @param mode The autowireAll to set.
     */
    public void setDefaultAutowireMode(int mode) {
        if (mode != 0 && mode != AutowireCapableBeanFactory.AUTOWIRE_BY_NAME &&
                mode != AbstractAutowireCapableBeanFactory.AUTOWIRE_BY_TYPE) {
            throw new IllegalArgumentException("defaultAutowireMode must be a constant on AutowireCapableBeanFactory: AUTOWIRE_BY_TYPE or AUTOWIRE_BY_NAME");
        }
        defaultAutowireMode = mode;
    }

    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        if (beanFactory instanceof AutowireCapableBeanFactory) {
            this.aabf = (AutowireCapableBeanFactory)beanFactory;
        }
    }

    /**
     * Expose the owning bean factory to subclasses.
     * Call only after initialization is complete.
     *
     * @return the owning bean factory. Cannot be null in valid use of this class.
     */
    protected BeanFactory getBeanFactory() {
        return this.beanFactory;
    }

    /**
     * Set the Classes or class names that will be autowired by type
     *
     * @param autowireByTypeClasses list of Class or String classname
     */
    public void setAutowireByTypeClasses(List autowireByTypeClasses) {
        this.autowireByTypeClasses = convertListFromStringsToClassesIfNecessary(autowireByTypeClasses);
    }

    /**
     * Return classes autowired by type
     *
     * @return list of Class
     */
    public List getAutowireByTypeClasses() {
        return autowireByTypeClasses;
    }

    public void addAutowireByTypeClass(Class clazz) {
        this.autowireByTypeClasses.add(clazz);
    }


    /**
     * Set the Classes or class names that will be autowired by name
     *
     * @param autowireByNameClasses list of Class or String classname
     */
    public void setAutowireByNameClasses(List autowireByNameClasses) {
        this.autowireByNameClasses = convertListFromStringsToClassesIfNecessary(autowireByNameClasses);
    }


    /**
     * Return classes autowired by name
     *
     * @return list of Class
     */
    public List getAutowireByNameClasses() {
        return autowireByNameClasses;
    }


    public void addAutowireByNameClass(Class clazz) {
        this.autowireByNameClasses.add(clazz);
    }


    /**
     * Property key is class FQN, value is prototype name to use to obtain a new instance
     *
     * @param persistentClassBeanNames
     */
    public void setManagedClassNamesToPrototypeNames(Properties persistentClassBeanNames) {
        for (Iterator i = persistentClassBeanNames.keySet().iterator(); i.hasNext();) {
            String className = (String)i.next();
            String beanName = persistentClassBeanNames.getProperty(className);
            addManagedClassToPrototypeMapping(classNameStringToClass(className), beanName);
        }
    }

    /**
     * Utility method to convert a collection from a list of String class name to a list of Classes
     *
     * @param l list which may contain Class or String
     * @return list of resolved Class instances
     */
    private List convertListFromStringsToClassesIfNecessary(List l) {
        List classes = new ArrayList(l.size());
        for (Iterator itr = l.iterator(); itr.hasNext();) {
            Object next = itr.next();
            if (next instanceof String) {
                next = classNameStringToClass((String)next);
            }
            classes.add(next);
        }
        return classes;
    }


    /**
     * Resolve this FQN
     *
     * @param className name of the class to resolve
     * @return the Class
     */
    private Class classNameStringToClass(String className) {
        ClassEditor ce = new ClassEditor();
        ce.setAsText(className);
        return (Class)ce.getValue();
    }

    /**
     * Return a Map of managed classes to prototype names
     *
     * @return Map with key being FQN and value prototype bean name to use for that class
     */
    public Map getManagedClassToPrototypeNames() {
        return this.managedClassToPrototypeMap;
    }

    public void addManagedClassToPrototypeMapping(Class clazz, String beanName) {
        this.managedClassToPrototypeMap.put(clazz, beanName);
    }

    /**
     * Check that mandatory properties were set
     *
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public final void afterPropertiesSet() {
        if (beanFactory == null) {
            throw new IllegalArgumentException("beanFactory is required");
        }

        validateConfiguration();
        validateProperties();
    }

    /**
     * Subclasses should implement this to validate their configuration
     */
    protected abstract void validateProperties();

    protected void validateConfiguration() {
        if (managedClassToPrototypeMap.isEmpty() && autowireByTypeClasses.isEmpty() &&
                autowireByNameClasses.isEmpty() &&
                defaultAutowireMode == 0) {
            throw new IllegalArgumentException("Must set persistent class information: no managed classes configured and no autowiring configuration or defaults");
        }

        if ((defaultAutowireMode != 0 || !autowireByTypeClasses.isEmpty() ||
                !autowireByNameClasses.isEmpty()) &&
                aabf == null) {
            throw new IllegalArgumentException("Autowiring supported only when running in an AutowireCapableBeanFactory");
        }

        // Check that all persistent classes map to prototype definitions
        for (Iterator itr = managedClassToPrototypeMap.keySet().iterator(); itr.hasNext();) {
            String beanName = (String)managedClassToPrototypeMap.get(itr.next());
            if (!beanFactory.containsBean(beanName)) {
                throw new IllegalArgumentException("No bean with name '" + beanName + "' defined in factory");
            }
            if (beanFactory.isSingleton(beanName)) {
                throw new IllegalArgumentException("Bean name '" + beanName + "' must be a prototype, with singleton=\"false\"");
            }
        }
        logger.info("Validated " + managedClassToPrototypeMap.size() + " persistent class to prototype mappings");
    }

    /**
     * Subclasses can call this to autowire properties on an existing object
     *
     * @param o
     * @throws BeansException
     */
    protected void autowireProperties(Object o) throws NoAutowiringConfigurationForClassException, BeansException {
        if (autowireByTypeClasses.contains(o.getClass())) {
            aabf.autowireBeanProperties(o, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
        } else if (autowireByNameClasses.contains(o.getClass())) {
            aabf.autowireBeanProperties(o, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false);
        } else if (defaultAutowireMode == AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE) {
            aabf.autowireBeanProperties(o, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
        } else if (defaultAutowireMode == AutowireCapableBeanFactory.AUTOWIRE_BY_NAME) {
            aabf.autowireBeanProperties(o, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false);
        } else {
            throw new NoAutowiringConfigurationForClassException(o.getClass());
        }
        logger.info("Autowired properties of persistent object with class=" + o.getClass().getName());
    }

    /**
     * Subclasses will call this to create an object of the requisite class
     *
     * @param clazz
     * @return
     * @throws NoAutowiringConfigurationForClassException
     *
     */
    protected Object createAndConfigure(Class clazz)
            throws NoAutowiringConfigurationForClassException {
        Object o = null;
        String name = (String)managedClassToPrototypeMap.get(clazz);
        if (name != null) {
            o = beanFactory.getBean(name);
        } else {
            // Fall back to trying autowiring
            o = BeanUtils.instantiateClass(clazz);
            autowireProperties(o);
        }

        if (o == null) {
            throw new NoAutowiringConfigurationForClassException(clazz);
        } else {
            return o;
        }
    }


    protected class NoAutowiringConfigurationForClassException extends Exception {

        public NoAutowiringConfigurationForClassException(Class clazz) {
            super(clazz + " cannot be autowired");
        }
    }

}